package com.worley.argutils.bk;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.worley.argutils.bk.command.GetCommand;
import com.worley.argutils.bk.command.SetCommand;
import com.worley.argutils.config.CacheRegion;
import com.worley.argutils.config.RedisOpsConnectionFactory;

import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

@Component
@Command(name = "rbCommand")
public class RedisBenchmarkCommand implements Callable<Integer> {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private static final int MAX_CLIENTS = 200;
    
    @Resource
    private ConfigurableListableBeanFactory beanFactory;

    @Option(names = "-h", description = "Server hostname", defaultValue = "localhost")
    String hostname;

    @Option(names = "-p", description = "Server port (default 6379)", defaultValue = "6379")
    Integer port;

    @Option(names = "-a", description = "Password for Redis Auth")
    String password;
    
    @Option(names = "-t", description = "Only run the comma separated list of tests. The test names are the same as the ones produced as output.")
    List<String> tests;

    @Option(names = "-c", description = "Number of parallel connections (default 50)", defaultValue = "50")
    Integer clients;

    @Option(names = "-n", description = "Total number of requests (default 100000)", defaultValue = "100000")
    Integer requests;

    @Option(names = "-d", description = "Data size of SET/GET value in bytes (default 2)", defaultValue = "2")
    Integer size;

    public Integer call() throws Exception {
        logger.info("c={}, n={}, d={}, t={}", clients, requests, size, tests);

        if (clients > MAX_CLIENTS) {
            clients = MAX_CLIENTS;
        }

        try {

            RedisTemplate<String, String> redisOps = getRedisOps();
            List<Benchmark> allCommand = getAllCommand(redisOps);
            ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors
                    .newFixedThreadPool(clients);

            List<String> rs = new LinkedList<>();
            rs.add("命令 \t 毫秒 \t QPS");

            for (Benchmark bk : allCommand) {
                for (int i = 0; i < clients; i++) {
                    executor.execute(bk);
                }

                int seconds = 0;
                do {
                    TimeUnit.SECONDS.sleep(1);
                    int reqs = bk.getRunState().getTotalReq();
                    long nanoTime = bk.getRunState().getTotalNanoTime();
                    logger.info("{}: Reqs = {}, Nano = {}, Avg = {}/s", bk.getName(),
                            reqs, nanoTime, reqs / ++seconds);
                } while (executor.getActiveCount() > 0);

                double avgMs = ((double) bk.getRunState().getTotalNanoTime())
                        / 1000000 / clients;
                String result = String.format("%s \t %s \t %s", bk.getName(), (long) avgMs,
                        (long) (requests / (avgMs / 1000)));
                rs.add(result);
                
                logger.info("--------------------------------------------");
                logger.info(rs.get(0));
                logger.info(result);
                logger.info("--------------------------------------------");
            }

            String rsTable = String.join("\n", rs);
            logger.info("\n\n客户端数={}, 请求数={}, 值长度={}\n\n{}", clients, requests,
                    size, rsTable);

        } catch (Exception e) {
            logger.error("执行失败：", e);
        }

        return 0;
    }
    
    private RedisTemplate<String, String> getRedisOps() {
        RedisOpsConnectionFactory rf = new RedisOpsConnectionFactory(beanFactory);
        for (CacheRegion cr : CacheRegion.values()) {
            try {
                RedisConnectionFactory c = rf.getRedisConnectionFactory(cr, 0);
                
                RedisTemplate<String, String> redisOps = new RedisTemplate<>();
                // 操作对象都按字符串类型
                redisOps.setDefaultSerializer(new StringRedisSerializer());
                redisOps.setConnectionFactory(c);
                redisOps.afterPropertiesSet();
                
                return redisOps;
            } catch (Exception e) {
                // ignore
            }
        }
        throw new RedisConnectionFailureException("请配置Redis连接的相关属性");
    }

    private List<Benchmark> getAllCommand(RedisTemplate<String, String> redisOps) {
        
        java.util.Set<String> all;
        if (CollectionUtils.isEmpty(tests)) {
            all = Collections.emptySet();
        } else {
            all = tests.stream().map(k -> k.toUpperCase().trim())
                    .filter(StringUtils::hasLength).collect(Collectors.toSet());
        }

        int avgCount = requests / clients;
        List<Benchmark> list = new LinkedList<>();
        if (all.isEmpty() || all.contains(SetCommand.NAME)) {
            String value = new String(new byte[size]);
            list.add(new SetCommand(redisOps, new RunState(avgCount, value)));
        }

        if (all.isEmpty() || all.contains(GetCommand.NAME)) {
            list.add(new GetCommand(redisOps, new RunState(avgCount, null)));
        }

        return list;
    }

}